13. Synchronization

Synchronization

In this section, you will learn what synchronization is, when to use it, and some different ways to use in in Java.

ND079 JPND C2 L05 A13a Synchronization V2

What is Synchronization?

As you saw in a previous section, it's possible for multiple threads to access shared state, such as a List or Map stored in the heap. It's also possible multiple threads could be accessing a shared resource, such as a file.

In the context of multi-threaded programming, synchronization is the process of limiting the number of threads that can access a shared resource at the same time.

Synchronization is actually a more general concept that covers more than just threads. For example you can limit concurrent access of multiple processes, or programs, to a file. In this lesson we will only be talking about threads.

When is Synchronization Needed?

You should think about synchronization whenever you have multiple threads accessing the same shared resource, such as a data structure or a file.

If all the threads are just reading the shared resource, that's usually okay without synchronization. This is sometimes called read-only access to the shared resource.

On the other hand, if one or both of the threads is updating, or writing to, the shared resource, synchronization may be required!

Which of the following scenarios may require synchronization between threads?

SOLUTION:
  • One thread reading from a file while the other writes.
  • Two threads writing to the same `TreeMap`

Ways to Synchronize

ND079 JPND C2 L05 A13b Synchronization Continued

Java has several built-in utilities to help you synchronize multi-threaded code. Here are two examples:

  • Synchronized collection wrappers:
Map<String, Integer> votes = Collections.synchronizedMap(new HashMap<>());
Map<String, Integer> votes = new ConcurrentHashMap<>();

What is one advantage of using ConcurrentHashMap over a "regular" non-concurrent Map wrapped with Collections.synchronizedMap()?

SOLUTION: `ConcurrentHashMap` is optimized to allow multi-threaded access without "blocking out" all but one thread.

Synchronization Demo: Voting App

ND079 JPND C2 L05 A14 Demo Synchronization V2

Code from the Demo

Single-Threaded Version

import java.util.*;
import java.util.concurrent.*;

public final class VotingApp {
  public static void main(String[] args) throws Exception {

    ExecutorService executor = Executors.newSingleThreadExecutor();

    Map<String, Integer> votes = new HashMap<>();

    List<Future<?>> futures = new ArrayList<>(10_000);
    for (int i = 0; i < 10_000; i++) {
      futures.add(
          executor.submit(() -> {
            votes.compute("Larry", (k, v) -> (v == null) ? 1 : v + 1);
          }));
    }
    for (Future<?> future : futures) {
      future.get();
    }
    executor.shutdown();

    System.out.println(votes);
  }
}

Multi-Threaded Version Using ConcurrentHashMap

import java.util.*;
import java.util.concurrent.*;

public final class VotingApp {
  public static void main(String[] args) throws Exception {

    ExecutorService executor = Executors.newFixedThreadPool(12);

    Map<String, Integer> votes = new ConcurrentHashMap<>();

    List<Future<?>> futures = new ArrayList<>(10_000);
    for (int i = 0; i < 10_000; i++) {
      futures.add(
          executor.submit(() -> {
            votes.compute("Larry", (k, v) -> (v == null) ? 1 : v + 1);
          }));
    }
    for (Future<?> future : futures) {
      future.get();
    }
    executor.shutdown();

    System.out.println(votes);
  }
}

Multi-Threaded Version Using Collections.synchronizedMap()

import java.util.*;
import java.util.concurrent.*;

public final class VotingApp {
  public static void main(String[] args) throws Exception {

    ExecutorService executor = Executors.newFixedThreadPool(12);

    Map<String, Integer> votes = Collections.synchronizedMap(new HashMap<>());

    List<Future<?>> futures = new ArrayList<>(10_000);
    for (int i = 0; i < 10_000; i++) {
      futures.add(
          executor.submit(() -> {
            votes.compute("Larry", (k, v) -> (v == null) ? 1 : v + 1);
          }));
    }
    for (Future<?> future : futures) {
      future.get();
    }
    executor.shutdown();

    System.out.println(votes);
  }
}

What are Atomic Operations?

An atomic operation is an operation that is executed as a single step, and cannot be split into smaller steps.

Atomicity is a really important concept in concurrent programming. If an operation is atomic, that means we don't have to worry about synchronizing it across different threads.

In the demo, you saw how ConcurrentHashMap.compute() is an atomic operation, but individually calling ConcurrentHashMap.get() and ConcurrentHashMap.put() is not atomic.

Considering the following Set, which set operations can be done atomically?

Set<String> set = Collections.synchronizedSet(new HashSet<>());
SOLUTION:
  • Checking the number of strings in the set.
  • Checking if a string `"c"` is in the set.
  • Checking if a string `"c"` is in the set and adding it if it's not already in the set.